package br.cin.ufpe.feedbacktest.core;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashSet;

public class Call {

	/**
	 * Internally call
	 * representation.
	 */
	private Object internalCall;

	/**
	 * Flag that determine if this
	 * call is a constructor or method.
	 */
	private byte typeFlag;

	/**
	 * Parameters
	 * Types to Call. 
	 */
	private Class<?>[] parameterTypes;

	/**
	 * Call
	 * Return Type. 
	 */
	private Class<?> returnType;

	/**
	 * Parameters
	 * Variables.
	 */
	private Variable[] parameterVariables;

	/**
	 * Return
	 * Variable.
	 */
	private Variable returnVariable;

	/**
	 * Constructor
	 * constant flag.
	 */
	private static final byte CONSTRUCTOR_TYPE_FLAG = 1;

	/**
	 * Method
	 * constant flag.
	 */
	private static final byte METHOD_TYPE_FLAG = 2;

	/**
	 * Call
	 * Constructor.
	 * 
	 * @param call The Java Call Object (Constructor or Method)
	 */
	public Call(Object call) {
		// Setting
		// Call Attribute
		this.internalCall = call;

		// Setting Declaring Class,
		// Type Flag, Parameter Types
		// and Return Type
		Class<?> callClass = this.internalCall.getClass();
		if (callClass.equals(Constructor.class)) {
			this.typeFlag = CONSTRUCTOR_TYPE_FLAG;			
			this.parameterTypes = ((Constructor<?>) call).getParameterTypes();		
			this.returnType = ((Constructor<?>) call).getDeclaringClass();

		} else if (callClass.equals(Method.class)) {
			this.typeFlag = METHOD_TYPE_FLAG;

			Class<?>[] methodParameters = ((Method) call).getParameterTypes();
			this.parameterTypes = new Class[methodParameters.length + 1];
			this.parameterTypes[0] =((Method) call).getDeclaringClass();
			for (int index = 1; index < this.parameterTypes.length; index++) {
				this.parameterTypes[index] = methodParameters[index - 1];
			}

			this.returnType = ((Method) call).getReturnType();
		}
	}

	public boolean isConstructor() {
		return (typeFlag == CONSTRUCTOR_TYPE_FLAG);
	}

	public boolean isMethod() {
		return (typeFlag == METHOD_TYPE_FLAG);	
	}
	
	public boolean isVoidMethod() {
		return (this.returnType.equals(void.class));
	}

	/**
	 * Method that return the
	 * parameter types to the call.
	 * 
	 * @return parameter types to the call
	 */
	public Class<?>[] getParameterTypes() {
		return this.parameterTypes;
	}

	/**
	 * Method that return the
	 * return type of the call.
	 * 
	 * @return return type of the call
	 */
	public Class<?> getReturnType() {
		return this.returnType;
	}

	/**
	 * Method that return the
	 * variable returned for this call.
	 * 
	 * @return variable returned for this call
	 */
	public Variable getReturnVariable() {
		return this.returnVariable;
	}

	/**
	 * Method to set the
	 * Parameter Variables.
	 * 
	 * @param parameters Parameter Variables
	 */
	public void setParameters(Variable[] parameters) {
		this.parameterVariables = parameters;
	}

	/**
	 * Method to initialize
	 * the Return Variable.
	 */
	public void initReturn() {
		if (!isVoidMethod()) {
			this.returnVariable = new Variable();
			this.returnVariable.setName(VariableNamer.nextName());
			this.returnVariable.setType(this.returnType);
		}
	}
	
	public void clearVariables() {
		if (!isVoidMethod()) {
			returnVariable.setValue(null);
		}
		
		for (int index = 0; index < parameterVariables.length; index++) {
			if (parameterVariables[index].getName() == null && !parameterVariables[index].isPrimitive()) {
				parameterVariables[index].resetInstance();
			}
		}
	}

	/**
	 * Method that
	 * to the call.
	 * 
	 * @return true if any exception occurs, false otherwise
	 */
	public boolean execute() {
		boolean violated = false;

		// Call Parameters
		// Variable[] to Object[]
		Object[] parameters = new Object[this.parameterVariables.length];
		for (int index = 0; index < parameters.length; index++) {
			parameters[index] = this.parameterVariables[index].getValue();
		}

		try {
			if (this.typeFlag == CONSTRUCTOR_TYPE_FLAG) {
				// Running Constructor and
				// Storing Instantiated Object
				Constructor<?> constructor = (Constructor<?>)this.internalCall;
				Object o = constructor.newInstance(parameters);
				this.returnVariable.setValue(o);

			} else if (this.typeFlag == METHOD_TYPE_FLAG) {
				Method method = (Method)this.internalCall;

				// Building
				// Method Parameters
				Object[] methodArgs = new Object[parameters.length - 1];
				for (int i = 0; i < methodArgs.length; i++) {
					methodArgs[i] = parameters[i + 1];
				}

				// Running Method and
				// Storing Runtime Value
				Object o = method.invoke(parameters[0], methodArgs);
				if (!isVoidMethod()) {
					this.returnVariable.setValue(o);
				}
			}

		} catch (Throwable exception) {			
			violated = true;
		}

		return violated;
	}

	public HashSet<String> getImports() {
		HashSet<String> imports = new HashSet<String>(3);		

		for (int i = 0; i < parameterTypes.length; i++) {
			if (!parameterTypes[i].isPrimitive() && !parameterTypes[i].equals(void.class)) {
				if(parameterTypes[i].isArray()){
					imports.add(parameterTypes[i].getComponentType().getName());
				}else{
					imports.add(parameterTypes[i].getName());
				}
			}

		}

		if (!returnType.isPrimitive() && !returnType.equals(void.class)) {
			imports.add(returnType.getName());
		}

		return imports;
	}

	/**
	 * Equals
	 * Method.
	 */
	public boolean equals(Object object) {
		boolean equals = false;

		if (object instanceof Call) {
			Call call = (Call)object;			
			//equals = this.internalCall.toString().equals(call.internalCall.toString());
			equals = this.internalCall.equals(call.internalCall);
		}

		return equals;
	}

	/**
	 * toString
	 * Method.
	 */
	public String toString() {
		StringBuffer buffer = new StringBuffer();

		buffer.append("        ");
		if (this.typeFlag == CONSTRUCTOR_TYPE_FLAG) {
			buffer.append(this.returnType.getName());
			buffer.append(" ");
			buffer.append(this.returnVariable.getName());
			buffer.append(" = new ");
			buffer.append(this.returnType.getName());

			buffer.append("(");
			for (int i = 0; i < this.parameterVariables.length; i++) {
				buffer.append(this.parameterVariables[i]);
				if (i != this.parameterVariables.length - 1) {
					buffer.append(", ");
				}
			}
			buffer.append(");");

		} else if (this.typeFlag == METHOD_TYPE_FLAG) {
			if (!this.returnType.equals(void.class)) {

				if (this.returnType.isArray()) {
					buffer.append(this.returnType.getComponentType().getName());
					buffer.append("[]");
				} else {
					buffer.append(this.returnType.getName());
				}

				buffer.append(" ");
				buffer.append(this.returnVariable);
				buffer.append(" = ");
			}

			buffer.append(this.parameterVariables[0]);
			buffer.append(".");
			buffer.append(((Method) this.internalCall).getName());

			buffer.append("(");
			for (int i = 1; i < this.parameterVariables.length; i++) {
				buffer.append(this.parameterVariables[i]);
				if (i != this.parameterVariables.length - 1) {
					buffer.append(", ");
				}
			}
			buffer.append(");");
		}

		return buffer.toString();
	}

}
